package lab_14.com.acme.bankapp.service.bank;

import lab_14.com.acme.bankapp.domain.bank.*;
import lab_14.com.acme.bankapp.domain.bank.Exceptions.*;

import java.io.*;
import java.util.*;
import java.util.regex.*;

public class BankDataLoaderService {

	/**
	 * Save all bank data to the file
	 * 
	 * @param bank
	 *            Bank object instance from which the data will be taken for
	 *            saving
	 * @param fileName
	 *            name of the file the data to be saved to
	 * @throws IOException
	 * @throws DataFileLoadingException 
	 */
	public static void saveBankToFeed(Bank bank, String fileName) throws DataFileLoadingException {
		FileOutputStream fos = null;
		BufferedOutputStream bos = null;
		OutputStreamWriter osw = null;
		

		try {
			fos = new FileOutputStream(fileName);
			try {
				bos = new BufferedOutputStream(fos);
				try {
					osw = new OutputStreamWriter(bos);
					// Actual writing happens here
					osw.write(bank.getBankStreamData());
				} finally {
					osw.close();
				}
			} finally {
				bos.close();
			}

		} catch (IOException e) {
			throw new DataFileLoadingException(e);
		} finally {
			try {
				if (fos!=null) fos.close();
			} catch (IOException e) {
				throw new DataFileLoadingException(e);
			}
		}
	}

	/**
	 * Update information in the existing bank from feed.</br> This method is
	 * useful when you already have an existing bank object with Listeners set.
	 * 
	 * @param bank
	 *            Bank object instance to which the data will be loaded from the
	 *            file
	 * @param fileName
	 * @throws DataFileLoadingException
	 */
	public static void updateBankFromFileFeed(Bank bank, String fileName) throws DataFileLoadingException {
		FileInputStream fis = null;
		BufferedInputStream bis = null;
		InputStreamReader isr = null;
		

		try {
			fis = new FileInputStream(fileName);
			try {
				bis = new BufferedInputStream(fis);
				try {
					isr = new InputStreamReader(bis);
					// Processing happens here
					updateBankFromFeedStream(bank, isr);
				} finally {
					isr.close();
				}
			} finally {
				bis.close();
			}
		} catch (IOException e) {
			throw new DataFileLoadingException(e);
		} finally {
			try {
				if (fis!=null) fis.close();
			} catch (IOException e) {
				throw new DataFileLoadingException(e);
			}
		}

	}
	
	/**
	 * Update information in the existing bank from feed.</br> This method is
	 * useful when you already have an existing bank object with Listeners set.
	 * 
	 * @param bank
	 *            Bank object instance to which the data will be loaded from the
	 *            file
	 * @param fileName
	 * @throws DataFileLoadingException
	 */
	public static void updateBankFromStringFeed(Bank bank, String string) throws DataFileLoadingException {
		InputStream is = null;
		BufferedInputStream bis = null;
		InputStreamReader isr = null;
		

		try {
			is = new ByteArrayInputStream(string.getBytes());
			try {
				bis = new BufferedInputStream(is);
				try {
					isr = new InputStreamReader(bis);
					// Processing happens here
					updateBankFromFeedStream(bank, isr);
				} finally {
					isr.close();
				}
			} finally {
				bis.close();
			}
		} catch (IOException e) {
			throw new DataFileLoadingException(e);
		} finally {
			try {
				if (is!=null) is.close();
			} catch (IOException e) {
				throw new DataFileLoadingException(e);
			}
		}

	}

	/**
	 * Create a new bank from a feed.
	 * 
	 * @param fileName
	 * @return
	 * @throws DataFileLoadingException
	 */
	public static Bank readBankFromFileFeed(String fileName) throws DataFileLoadingException {
		Bank bank = new Bank();
		updateBankFromFileFeed(bank, fileName);
		return bank;
	}

	/**
	 * Read the data to the bank from an opened InputStreamReader
	 * 
	 * @param bank
	 *            Bank to save data to
	 * @param isr
	 *            Opened file object to be parsed
	 * @throws IOException
	 * @throws DataFileLoadingException
	 */
	public static void updateBankFromFeedStream(Bank bank, InputStreamReader isr) throws IOException,
			DataFileLoadingException {
		// Key-value pattern
		Pattern patKeyEqVal = Pattern.compile("(?<name>\\w+)=(?<value>[\\w,.-]+);");

		// Expected sets of the arguments
		final Set<String> checkingAccountArguments = new HashSet<String>(Arrays.asList("accounttype",
				"balance", "overdraftlimit", "name", "gender"));
		final Set<String> savingAccountArguments = new HashSet<String>(Arrays.asList("accounttype",
				"balance", "name", "gender"));

		LineNumberReader lnr = new LineNumberReader(isr);
		String line;
		HashMap<String, String> keyValue = new HashMap<String, String>();
		try {
			while ((line = lnr.readLine()) != null) {
				Matcher match = patKeyEqVal.matcher(line);
				keyValue.clear(); // clear hasMap for the data from the new
									// string

				// Store the found data from the string into the keyValue
				// hashmap
				while (match.find()) {
					keyValue.put(match.group("name"), match.group("value"));
				}

				// Processing differs on account type, so we process it first.
				if (!keyValue.containsKey("accounttype")) {
					throw new DataFileLoadingException(String.format(
							"The content file doesn't have accounttype property on the line %d",
							lnr.getLineNumber()));
				}

				String accountType = keyValue.get("accounttype");

				// Checking for the set of parameters to be matching the
				// expected sets
				switch (accountType) {
				case ("s"):
					if (!keyValue.keySet().equals(savingAccountArguments))
						throw new DataFileLoadingException(
								String.format(
										"The content file has a wrong list of arguments for saving account on the line  %d",
										lnr.getLineNumber()));
					break;
				case ("c"):
					if (!keyValue.keySet().equals(checkingAccountArguments))
						throw new DataFileLoadingException(
								String.format(
										"The content file has a wrong list of arguments for checking account on the line %d",
										lnr.getLineNumber()));
					break;
				default:
					throw new DataFileLoadingException(String.format(
							"The content file has a wrong accounttype property on the line %d",
							lnr.getLineNumber()));
				}

				// Get the existing client in the bank or a newly created
				// matching the name of file being processed.
				Client client = bank.updateClient(Gender.getBySerialName(keyValue.get("gender")),
						keyValue.get("name"));

				// Account is always added (as a client may have several equal
				// accounts)
				Account account = null;
				switch (accountType) {
				case ("s"):
					account = new SavingsAccount(Double.parseDouble(keyValue.get("balance")));
					break;
				case ("c"):
					account = new CheckingAccount(Double.parseDouble(keyValue.get("balance")),
							Double.parseDouble(keyValue.get("overdraftlimit")));
					break;
				}

				// Add the account to the client
				client.addClientAccount(account);

				// Output to sysout
//				System.out.printf(
//						"Client info is read from file line %d. Displaying new client info.\n",
//						lnr.getLineNumber());
//				client.printAllClientData();

			}
		} catch (Exception e) {
			// This processes the case when something unexpected went wrong
			// (there may be a lot of sources of errors, e.g. from parsing
			// doubles etc.)
			e.printStackTrace();

			// Propagate the exception up the stack
			throw new DataFileLoadingException(
					String.format("Someting went wrong while reading content file on the line %d",
							lnr.getLineNumber()), e);
		} finally {
			lnr.close();
		}

	}

	public static void serializeBankToFile(Bank bank, String fileName) throws DataFileLoadingException {
		FileOutputStream fos = null;
		BufferedOutputStream bos = null;
		ObjectOutputStream oos = null;
		

		try {
			fos = new FileOutputStream(fileName);
			try {
				bos = new BufferedOutputStream(fos);
				try {
					oos = new ObjectOutputStream(bos);
					oos.writeObject(bank);
				} finally {
					if (oos != null)
						oos.close();
				}
			} finally {
				if (bos != null)
					bos.close();
			}

		} catch (IOException e) {
			throw new DataFileLoadingException(e);
		} finally {
			if (fos != null)
				try {
					fos.close();
				} catch (IOException e) {
					throw new DataFileLoadingException(e);
				}

		}
	}

	public static Bank deserializeBankFromFile(String fileName) throws DataFileLoadingException {
		FileInputStream fis = null;
		BufferedInputStream bis = null;
		ObjectInputStream ois = null;
		Bank bank = null;
		
		System.out.println("Deserializing bank from file.");

		try {
			fis = new FileInputStream(fileName);
			try {
				bis = new BufferedInputStream(fis);
				try {
					ois = new ObjectInputStream(bis);
					try {
						bank = (Bank) ois.readObject();
					} catch (Exception e) {
						throw new DataFileLoadingException(
								"Something went wrong with reading bank from serializable file", e);
					}
				} finally {
					if (ois != null)
						ois.close();
				}
			} finally {
				if (bis != null)
					bis.close();
			}
		} catch (IOException e) {
			throw new DataFileLoadingException(e);
		} finally {

			if (fis != null)
				try {
					fis.close();
				} catch (IOException e) {
					e.printStackTrace();
				}

		}
		
		System.out.println("Bank is read from serialization file.");
		bank.printBankData();
		return bank;
	}

}
